In this notebook, LTSpice will be used to analyze the operation of the pedal using device models for the Op Amps and diodes. The Op Amp and diode device models describe the nonlinear operation of these devices and LTSpice is capable of performing nonlinear analysis, something that the Python MNA can’t do.
The results from LTSpice are saved as a comma separated value (CSV) file and imported into this notebook for plotting and comparison with the Python results. Figure 38.1 shows the schematic used in the LTSpice analysis. The circuit is a redrawn version of Martin Chittum’s Klon Centaur schematic shown in Figure 37.2 except that the ICL7660 and its associated components have been replaced by fixed voltage sources, V2, V3, V4 and V5.
The motivating reason for employing LTSpice for this part of the analysis is to explore the effects of the non-linear components. The Diodes, D1 and D2, are the primary components that introduce distortion into the signal path. Under the combined conditions of moderate gain setting on P1 and relatively large drive input, the Op Amp U1b can be driven into saturation. When this occurs the output tone will be a combination of non-linearities produced by the clipping of the signal by the Op Amp as the signal levels are driven beyond the power supply rails for U1b and the signal limiting resulting from the diodes.
Given the mystic and alleged magical ability of some types of Germanium diodes to produce superior musical sounds, it seems that Op Amp saturation should be avoided, otherwise the sound will be muddled and negating the point and argument for “special” Germanium diodes.
The analysis presented in this chapter will start with a description of the schematic in Figure 37.2. The Op Amps and Germanium diodes are discussed. Then LTSpice was used to do a parameter sweep where the input signal level was varied from 0.1 volts to 1.5 volts while the gain was varied from 1\(\Omega\) to 99 k\(\Omega\). The voltages at various points in the circuit were plotted versus these parameters. Next the circuit was examined with sinusoidal input signals of various amplitudes and the harmonic content of the diode limiting was examined. Actual musical signals were also used as input to the simulation since LTSpice has the ability to read wav files. Finally a comparison of LTSpice results versious Python MNA circuit analysis results is presented to validate the Python MNA analysis.
The following Python modules are used in this notebook.
Code
from sympy import*import numpy as npfrom scipy import signal#from scipy.signal import blackmanimport scipy.fftimport matplotlib.pyplot as pltimport matplotlib.tickerimport pandas as pdimport SymMNAfrom IPython.display import display, Markdown, Math, Latexinit_printing()from tabulate import tabulate
38.1 Non-linear analysis with LTSpice
The schematic shown below was drawn in LTSpice. The voltage converter, U3, in Figure 37.2, has been replaced by the voltage sources, \(V2\), \(V3\), \(V4\) and \(V5\). Voltage sourcee, \(V1\), is the guitar input signal. The effects bypass switch modeled by setting \(R_{26}\) to 68k \(\Omega\) and \(R_{27}\) to 0.01 \(\Omega\), which models switch S1A shorting \(R_{27}\) to the wiper terminal of P3. All of the resistors and capacitors are assumed to be ideal. The schematic includes power supply connections for the Op Amps. In the analysis that follows, I’ll be looking at conditions where the Op Amps might be driven into saturation.
Figure 38.1: Schematic of Klon Centuar used for the LTSpice nonlinear analysis.
38.2 Semiconductor components
Within the Klon Centuar’s circuits there are several semiconductor devices used. These are the diodes, Op Amps and voltage converter. This section will describe these components and thier use in the circuit.
38.2.1 Dual High Slew Rate JFET Input Op Amp - TL072
The operational amplifier (Op Amp) used in the pedal is the TL072, available from Texas Instruments. The TL072 is a dual, high slew rate, JFET-input Op Amp and is part of a family of industry-standard devices (TL071, TL072, and TL074). There are next-generation versions available (TL071H, TL072H, and TL074H). The TL072 product continues to be available for existing customers, but new designs should consider an alternate product.
The TL072 provides outstanding value for cost-sensitive applications, and features include: low offset (1 mV, typical), high slew rate (20 V/µs), and common-mode input to the positive supply. High ESD (1.5 kV, HBM), integrated EMI and RF filters, and operation across the full –40°C to 125°C enable the TL07xH devices to be used in the most rugged and demanding applications.
The clipping diodes, D1 and D2, are type 1N34A, a general purpose point contact Germanium diode. As recounted in interviews with Bill Filigan, many versions of the 1N34A from various vendors were evaluated and 1N34A diodes from one particular source were selected based on listening tests. Bill has not disclosed the exact source of the 1N34A diodes he used, so this aspect of the Klon Centaur has remained a trade secret. It has been reported that Bill’s initial stock of Germanium diodes have been depleted and new production uses diodes from a new source.
While the electrical characteristics of Germanium semiconductor devices is well understood, the sonic properties of audio circuits using Germanium semiconductor devices have been described as being warm and more musical. Accordingly, some mystic and urban lour have come to be associated with the use of Germanium diodes and transistors. Most online commenters seem to focus on the forward voltage drop of Germanium diodes along with some sonic qualities. Here are two excerpts:
Me and Dylan and David from the DIY guitar pedal/effects community decided to see for ourselves what the fuzz is about regarding diodes and their “softness”. Why do people like Germaniums over Silicons? What is the effect of multiple diodes in series? How exactly does the PN junction of a MOSFET look like? All these questions and more will be answered.
When you get into vintage effect pedals etc. you might get to know the amazing germanium diode. These diodes are truly worse in every way compared to silicon diodes, and thus aren’t being produced anymore. This makes them a treasure for those who are chasing the vintage tone. Luckily here and there germanium diode surpluses are still being sold with nice profit margins.
But what is so special about germanium? Well, people claim that they clip “softer” than silicon diodes. This makes your tone sound “smoother” and whatever. In my post about testing diodes it is clear that the knee (slope) of germanium diodes is indeed smoother than those of silicon diodes so that’s one way to prove that it has “some” impact on your tone. But can’t we emulate this soft knee in some way?
The blogpost also showed how you can simply put diodes in series to double the forward voltage, but also make the knee softer. Putting 4 or more silicons in series gets you close to the softness of a germanium, but with a forward voltage of 2.8V.
My analysis of the circuits used in the Klon Centaur cannot evaluate the sonic aspects of Germanium diodes, something that could be properly investigated by building prototype circuits and doing blind listen tests, but instead will focus on the general electrical characteristics of the 1N34A. The 1N34A diode model used in the LTSpice simulations is:
The circuit shown below is used to evaluate the 1N34A model.
Figure 38.2: Schematic of LTSpice diode test jig.
The following code loads the voltage versus current data for the 1N34A Germanium diode and the 1N914 Silicon diode models.
Code
fn ='Diode-vi-curve.csv'# data from LTSpiceLTSpice_data = np.genfromtxt(fn, delimiter=',',skip_header=1)
Copy the data from the csv file into NumPy arrays.
Code
# initaliase some empty arraysD_1N34A_voltage = np.zeros(len(LTSpice_data))D_1N34A_diode_current = np.zeros(len(LTSpice_data))D_1N914_voltage = np.zeros(len(LTSpice_data))D_1N914_diode_current = np.zeros(len(LTSpice_data))# load csv data into the arrayfor i inrange(len(LTSpice_data)): D_1N34A_voltage[i] = LTSpice_data[i][0] D_1N34A_diode_current[i] = LTSpice_data[i][1]*1000 D_1N914_voltage[i] = LTSpice_data[i][2] D_1N914_diode_current[i] = LTSpice_data[i][3]*1000
Plot the LTSpice simulated forward voltage versus current relationship for 1N34A and 1N914 diode models.
Code
fig1, ax1 = plt.subplots()ax1.plot([10, 100, 1000], [1,2,3])ax1.text(D_1N34A_voltage[1375], D_1N34A_diode_current[1375], '{:.2f} volts, {:.2f} mA '.format(D_1N34A_voltage[1375], D_1N34A_diode_current[1375]), fontsize=8, horizontalalignment='right', verticalalignment='center')ax1.plot(D_1N34A_voltage, D_1N34A_diode_current,'-b',label='1N34A')ax1.plot(D_1N34A_voltage[1375], D_1N34A_diode_current[1375],'xb')ax1.plot(D_1N914_voltage, D_1N914_diode_current,'-r',label='1N914')ax1.set_xlim(0.1,1)ax1.set_ylim(0,10)ax1.set_xlabel('volts')ax1.set_ylabel('current. mA')ax1.set_xscale('log')ax1.set_xticks([0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 1.0])ax1.get_xaxis().set_major_formatter(matplotlib.ticker.ScalarFormatter())ax1.get_xaxis().set_tick_params(which='minor', size=0)ax1.get_xaxis().set_tick_params(which='minor', width=0)ax1.grid()ax1.legend()plt.title('Forward voltage versus current for 1N34A and 1N914 diode models')plt.show()
Figure 38.3: Voltage vs current plot for 1N34A and 1N914 diodes.
As shown in the plot above, the VI curves for the 1N34A and 1N914 diode models are different. The 1N34A has a forward voltage of 0.38 volts at 5mA, whereas the 1N914 had a forward voltage of about 0.62 volts at 5mA. The shape of the two curves is also different with the 1N34A curve being a bit more rounded. The harmonics generated in audio circuits using the 1N34A will have a different set of amplitude coefficients and a different sound.
38.2.3 CMOS Switched Capacitor Voltage Converter - ICL7660
The electrical power to operate the Klon Centuar comes from the DC jack or the internal 9 volt battery. Nominally both the DC jack or the battery are 9 volt sources, however as the battery drains, the voltage will be less than 9 volts and the power source conneccted to the DC jack might be slightly different than 9 volts. The 9 volt power source is filtered by C17 and D4 provides reverse voltage protection. Internal to the Klon Centuar there are four power supply rails derived from the 9 volt battery or the DC jack. The 9 volts is divided by two using resistors R29 and R30 to produce +4.5V and C18 stabilizes this voltage.
U3 is CMOS switched capacitor voltage converter, part number ICL7660, which converts positive 9 volts into -9 and +18 volts. The ICL7660, with a few external componets are monolithic, can double, divide, or multiply a positive input voltage. The ICL7660 will operate from 1.5V to 10V input voltage and can deliver upto 10mA with a 0.5V output drop. The ICL7660 is currently produced by Renesas Electronics Corporation and Analog Devices Inc./Maxim Integrated, and distributed by Digikey under the part numbers, ICL7660CPAZ and ICL7660CPA+.
The Op Amps U1 and U2 are connected to various supply rails. U1a and U1b power supply voltages are 9 volts and ground. The signal ground is a vertual ground with a voltage of 4.5 volts. This allows audio signals processed by U1 to swing between about 2 volts and 7 volts peak to peak without clipping. The power supply levels for U2A and U2B are 18 volts and -9 volts. This allows signals processed by U2 to swing between about -7 to +16 volts peak to peak without clipping.
38.3 Input Voltage and Gain Sweep
LTSpice was setup to run a series of simulations which stepped though a list of \(V1\) amplitudes and values for \(R_{gain}\). The frequency of \(V1\) was ste to 1 kHz since this is the frequency where some some reseonat paths peak. The voltage range used for \(V1\) is 0.1 volts to 1.5 volts. The following SPICE commands were used:
There are about 354,000 records in the Pandas dataframe. Voltage data was collected from the nodes listed in Table 38.1 and a short description of the node’s location in the circuit is provided.
Table 38.1: Nodes and connections
Node
Description
7
U1A output
14
U1B output
17
right side of C10
19
U2A output
21
U2B output
A sample of the the first 10 records in the dataframe are displayed below. Each simulation run for the various \(V1\) gains and \(R_{gain}\) setting is delimited by Step Information: ....
Code
LTSpice_sweep_df.head(5)
time
V(7)
V(14)
V(17)
V(19)
V(21)
0
Step Information: V1_amp=100m Rgain=1 (Step: ...
NaN
NaN
NaN
NaN
NaN
1
0
4.499977
4.500065
4.500011
4.50035
4.499677
2
1.25000001460762E-08
4.499977
4.500065
4.500011
4.50035
4.499677
3
2.50000002921524E-08
4.499977
4.500065
4.500011
4.50035
4.499677
4
5.00000005843049E-08
4.499977
4.500065
4.500011
4.50035
4.499677
In the csv file, there are lines of text for each new step in the simulation.
These lines are used to locate the index of the start of the data from each simulation sweep by looking at each recorded for the word ‘Step’ and a list of index values is created.
Code
lst = LTSpice_sweep_df['time'].tolist()step_index = []row_cnt =0for i in lst:if'Step'in i: step_index.append(row_cnt) row_cnt +=1
A new dataframe called sweep_df is created. The following code finds the peak to peak voltage at each node for each sweep and loads the values into the dataframe.
Node V14 is the output of U1B. This is the distortion path part A. As can been seen, an input of 1.5 volts is needed to saturate the Op Amp at the lowest gain setting. At a gain setting of 50%, an input of 0.75 volts peak will drive the Op Amp into saturation. Supply rail voltages for U1 are 9 volts and ground, with a virtual ground reference of 4.5 volts. This means that at some gain settings, it is likely that guitar signals with even small peak to peak amplitudes will drive \(U_1b\) into saturation.
The peak to peak values for \(v_{14}\) are plotted versus \(R_{gain}\) and \(V1\).
Code
for i in V1_amp_list: plt.plot(sweep_df[sweep_df['V1amp'] == i]['Rgain'].to_numpy(),sweep_df[sweep_df['V1amp'] == i]['V14pp'].to_numpy(),'-',label ='V1={:.2f}'.format(i))# position legend outside the graphplt.legend(bbox_to_anchor=(1.3,1)) # V1 legend position: relative (horizontal position, vertical position) plt.ylabel('U1 output, Vpp')plt.xlabel('Rgain value, k\u03A9')ax1.set_ylim((-0.5,6))plt.grid()plt.title('Node 14 Vpp levels versus $R_{gain}$ and $V1$')plt.show()
Figure 38.4: Node 14 peak to peak voltage versus \(R_{gain}\) and \(V1\) input.
As shown above, U1B, maximum output swing is about 6Vpp. This is because the power rails for \(U1\) are 0 and 9V and \(U1\) is not rail-to-rail capable. The SPICE model used for the TLO72 Op Amp reproduces the maximum output signal swing. If the gain pot, \(P1\) is set midpoint, signal inputs greater than 0.75V will drive U1B’s output into saturation as shown on the red curve above. This likely means that for mid range gain settings and loud guitar inputs, the guitar signal is first clipped by \(U1B\) and then again by the diodes. Any large harmonics generated by \(U1B\) will be mostly altered by \(D_1\) and \(D_2\).
38.3.2 Guitar Output Signal Levels
Since I don’t own a guitar and I’m not a guitar player, I searched online to find typical guitar signal levels. Various references such as Electric Guitar Output and Electric Guitar Output Voltage Levels placed the guitar output level in the range of 15\(\mu\)V to 740mV depending on playing style, string gauge and pickup type.
Table 38.2: Pickup Output Voltage - Averaged RMS (Peak)
Modified Maton
Neck (2.0kΩ)
Middle (N/A)
Bridge (2.0kΩ)
E1
40 mV (150mV)
32 mV (200mV)
E2
12 mV (120mV)
20 mV (300mV)
Chord
36 mV (200mV)
Average
29 mV (156 mV)
29 mV (267 mV)
Table 38.3: More Pickup Output Voltages
Samick ‘TV Twenty’
Neck (11.5kΩ)
Middle (11.3kΩ)
Bridge (15.3kΩ)
E1
44 mV (250mV)
76 mV (300 mV)
120 mV (800 mV)
E2
12 mV (50 mV)
12 mV (159 mV)
16 mV (200 mV)
Chord
76 mV (450 mV)
72 mV (400 mV)
128 mV (850 mV)
Average
44 mV (250 mV)
53 mV (283 mV)
88 mV (617 mV)
Tabulated results aren’t especially useful, for the simple reason that there will be huge variations due to playing style, and what’s being played. However, I did summarise the results. All numbers are millivolts (RMS) taken from the scope captures shown below. I didn’t include the bass, only the two guitars. Note that I use light gauge strings, and you will get more level with thicker ones. I don’t have a set for comparison, but I’d expect that you could get at least 6dB (×2) more when played hard. The pickup resistance is also shown in the table, not because it’s especially useful on its own, but you can make comparisons. It includes the parallel resistance of the volume control, as I didn’t feel like dismantling my guitars for a more accurate measurement.
So it would seem possible that when playing chords, some relatively high signal levels are produced by the guitar and that saturation of \(U1B\)’s output is likely. It seems likely to me that overdriving \(U1B\) was a design oversight since the tone of the pedal is claimed to emanate from the hand selected Germanium diodes.
In this section the transient analysis simulation results from LTSpice for the voltages at the diodes, \(v_{17}\) is examined by using NumPy and SciPy functions. The input stimulus to the Klon Centuar circuit is a 1kHz sine wave. The LTSpice simulation was run with various gain settings and input amplitude values.
A new Pandas data frame is declared with the colums names shown below. In this dataframe the node voltage versus time is tabulated for each inout level and gain setting.
Data is loaded into the new dataframe with the following code.
Code
for i inrange(0,len(step_index)):# row_elements = LTSpice_sweep_df['time'][step_index[i]].split()# convert the units of 'K' and 'm' to float Rgain_str = row_elements[3][6:] Rgain_value =float(Rgain_str.replace('K', 'e3')) Rgain_percent = Rgain_value/100e3*100 V1_amp_str = row_elements[2][7:] V1_amp_value =float(V1_amp_str.replace('m', 'e-3'))#if i ==0: time_values = LTSpice_sweep_df['time'][1:step_index[i+1]].to_numpy() v7_values = LTSpice_sweep_df['V(7)'][1:step_index[i+1]].to_numpy() v14_values = LTSpice_sweep_df['V(14)'][1:step_index[i+1]].to_numpy() v17_values = LTSpice_sweep_df['V(17)'][1:step_index[i+1]].to_numpy() v19_values = LTSpice_sweep_df['V(19)'][1:step_index[i+1]].to_numpy() v21_values = LTSpice_sweep_df['V(21)'][1:step_index[i+1]].to_numpy()elif (i !=0) and (i <len(step_index)-1): time_values = LTSpice_sweep_df['time'][step_index[i]+1:step_index[i+1]].to_numpy() v7_values = LTSpice_sweep_df['V(7)'][step_index[i]+1:step_index[i+1]].to_numpy() v14_values = LTSpice_sweep_df['V(14)'][step_index[i]+1:step_index[i+1]].to_numpy() v17_values = LTSpice_sweep_df['V(17)'][step_index[i]+1:step_index[i+1]].to_numpy() v19_values = LTSpice_sweep_df['V(19)'][step_index[i]+1:step_index[i+1]].to_numpy() v21_values = LTSpice_sweep_df['V(21)'][step_index[i]+1:step_index[i+1]].to_numpy()else: time_values = LTSpice_sweep_df['time'][step_index[i]+1:-1].to_numpy() v7_values = LTSpice_sweep_df['V(7)'][step_index[i]+1:-1].to_numpy() v14_values = LTSpice_sweep_df['V(14)'][step_index[i]+1:-1].to_numpy() v17_values = LTSpice_sweep_df['V(17)'][step_index[i]+1:-1].to_numpy() v19_values = LTSpice_sweep_df['V(19)'][step_index[i]+1:-1].to_numpy() v21_values = LTSpice_sweep_df['V(21)'][step_index[i]+1:-1].to_numpy()# load values into new dataframe td_node_voltage_df.loc[i] = [V1_amp_value, Rgain_percent,time_values,v7_values,v14_values,v17_values,v19_values,v21_values]
The first five records in the dataframe are displayed below.
Code
td_node_voltage_df.head(5)
V1amp
Rgain
time
V7
V14
V17
V19
V21
0
0.10
0.001
[0, 1.25000001460762E-08, 2.50000002921524E-08...
[4.499977, 4.499977, 4.499977, 4.499977, 4.499...
[4.500065, 4.500065, 4.500065, 4.500065, 4.500...
[4.500011, 4.500011, 4.500011, 4.500011, 4.500...
[4.50035, 4.50035, 4.50035, 4.50035, 4.50035, ...
[4.499677, 4.499677, 4.499677, 4.499677, 4.499...
1
0.25
0.001
[0, 6.25000007303811E-09, 1.25000001460762E-08...
[4.499977, 4.499977, 4.499977, 4.499977, 4.499...
[4.500065, 4.500065, 4.500065, 4.500065, 4.500...
[4.500011, 4.500011, 4.500011, 4.500011, 4.500...
[4.50035, 4.50035, 4.50035, 4.50035, 4.50035, ...
[4.499677, 4.499677, 4.499677, 4.499677, 4.499...
2
0.50
0.001
[0, 6.25000007303811E-09, 1.25000001460762E-08...
[4.499977, 4.499977, 4.499977, 4.499977, 4.499...
[4.500065, 4.500065, 4.500065, 4.500065, 4.500...
[4.500011, 4.500011, 4.500011, 4.500011, 4.500...
[4.50035, 4.50035, 4.50035, 4.50035, 4.50035, ...
[4.499677, 4.499677, 4.499677, 4.499677, 4.499...
3
0.75
0.001
[0, 6.25000007303811E-09, 1.25000001460762E-08...
[4.499977, 4.499977, 4.499977, 4.499977, 4.499...
[4.500065, 4.500065, 4.500065, 4.500065, 4.500...
[4.500011, 4.500011, 4.500011, 4.500011, 4.500...
[4.50035, 4.50035, 4.50035, 4.50035, 4.50035, ...
[4.499677, 4.499677, 4.499677, 4.499677, 4.499...
4
1.00
0.001
[0, 6.25000007303811E-09, 1.25000001460762E-08...
[4.499977, 4.499977, 4.499977, 4.499977, 4.499...
[4.500065, 4.500065, 4.500065, 4.500065, 4.500...
[4.500011, 4.500011, 4.500011, 4.500011, 4.500...
[4.50035, 4.50035, 4.50035, 4.50035, 4.50035, ...
[4.499677, 4.499677, 4.499677, 4.499677, 4.499...
At an offset of 127 in the dataframe, the gain setting is 60% and the inout amplitude is 0.25 volts. This will produce a signal at the diodes a bit larger than the conduction threshold and the signal will be slighly voltage limited as shown in the plot below.
Code
i=127print('Node 17, voltage vs input level V1amp={:.3f}, Rgain={:.0f}'.format(td_node_voltage_df.loc[i]['V1amp'],td_node_voltage_df.loc[i]['Rgain']))num_samples =len(td_node_voltage_df.loc[i]['time'])print(f'Number of samples in the simulation: {num_samples:,d}')
Node 17, voltage vs input level V1amp=0.250, Rgain=60
Number of samples in the simulation: 1,280
The bias voltage of 4.5 has been subtracted from the values in the plot.
Figure 38.5: Node 17, voltage at input level V1=0.25 and Rgain=60
The plot above shows the voltage at node 17. Somwhat evident is the rounding of the peaks of the waveform.
LTSpice returns the node voltages versus time where the sample interval is not uniform. LTSpice varies the time step to aid in the solution convergence. By plotting time data verus sample number we can see in the plot below, the function is not a straight line, but some what stair cased.
The harmonic content of the signal plotted in Figure 38.5 can be examined by the use of Fast Fourier transform (FFT). Since the simulation data ends abruptly, there is typically a non-zero value for the last data point in the simulation. A window function is typically used to minimize the “spectral leakage” associated with any discontinuities at the ends of the data set and helps to produce a more accurate representation of the signal’s true frequency content.
There are many window functions described in the signal processing literature. The window function used in this analysis is the Hann function and implemented by the NumPy function hanning. The Hann function is a taper formed by using a weighted cosine. The window is named after Julius von Hann, often referred to as Hanning. It is also known as the raised cosine, because of its similarity to a raised-cosine distribution.
The data, window and windowed data is plotted below.
Figure 38.7: Node 17 voltage, window and windowed data
The plot above overlays the voltage at node 17, the window function and the windowed data.
The following code diaplays variious parameters about the signal sample being analyized.
Code
num_samples =len(ynew)print(f'The number of samples is: {num_samples:,.0f}')sps = num_samples/stop_timeprint(f'The sample rate is: {sps:,.0f} samples per second')nyquist_freq = sps/2print(f'The Nyquist frequency: {nyquist_freq:,.0f} Hz')dt = stop_time/len(xnew)print(f'The sample interval: {dt*1e6}\u03BCs')
The number of samples is: 1,280
The sample rate is: 64,000 samples per second
The Nyquist frequency: 32,000 Hz
The sample interval: 15.625 μs
The SciPy library includes functions to compute discrete Fourier transforms, which are used in the code below to find the frequency components of the voltage at node 17. The SciPy function fft computes the one dimentional discrete Fourier Transform (DFT) using the Fast Fourier Transform (FFT) algorithm.
Code
yw_fft = scipy.fft.fft(yw)
The following code displays the frequency components of node 17 voltage. To display the amplitude spectrum in volts rms, take the two-sided amplitude amplitude spectrum and multiply the non-DC components by the square root of two and discard the second half of the array. The following equations shows the calculations from a two-sided FFT to a single-sided amplitude spectrum.
Code
yw_fft_ss = yw_fft[0:int(num_samples/2)] # use lower half of the arrayfreq = np.arange(int(num_samples/2))*sps/num_samplesk = np.ones(len(freq)) # Scaling factor is sqrt(2) except for at DC when it is 1k[1:] = k[1:]*sqrt(2)ywf_ss_Vrms_dB =20*np.log10(abs(2*k*yw_fft_ss/num_samples)) # 2 is the Hann window correction factorplt.plot(freq,ywf_ss_Vrms_dB)plt.xlim(0,10000)plt.ylim(-100,-10)plt.ylabel('amplitude, dBVrms')plt.xlabel('frequency, Hz')plt.title('FFT of the voltage at node 17')#plt.legend(loc='best')plt.grid()plt.show()
Figure 38.8: Node 17, frequency spectrum.
The frequency plot shows that odd harmonics are generated by the clipping action of the diodes.
Although LTSpice can calculate the FFT of voltages or currents, Python can also be used to dipaly the simulation results and perform additional analysis. As shown above, node voltages and branch currents can be extracted from LTSpice and brought into JupyterLab.
A useful feature of LTSpice is capability to use wav files as input signals to a circuit simulation. Actual guitar signals in the form of wav files were obtained from Sample Focus. The wav files are availabe under their Standard License and are free to use. I chose the file clean-electric-guitar-harmonic-reality.wav since I thought it represented someting close to the type of signals that a player would run through their Klon Centuar.
A transient analysis was performed using the file Klon-Centaur_v2_wavefile.asc and the input source, V1, wavefile = clean-electric-guitar-harmonic-reality.wav was declared as the value of V1. The simulation output was written to a wav file with the directive, .wave "klon_out.wav" 16 44.1k V(2). Using this file, the output of the circuit can be listened to on a computers audio player. The node voltages a several places were written to a csv file to be processed and plotted by Python in this notebook. The schematic includes a pre-amp labled as U1. The gain as been set to unity, but other gains can be obtained by adjusting the values of resistors \(R_{19}\) and \(R_{14}\).
The following Python modules are loaded into the JupyterLab enviroment inorder to read ‘wav’ files.
Code
from scipy.io import wavfileimport scipy.io
The simulation input file, clean-electric-guitar-harmonic-reality.wav, is loaded to the variable wav_input. In the input file has two chanels since it’s a steareo recording and these are the left and right channels. The length of the audio sample and the sample rate is displayed below.
Since the wave file containes stereo data, each channles is extracted for display.
Code
# split the data in channelschan0 = np.zeros(len(wav_input))chan1 = np.zeros(len(wav_input))for i inrange(len(wav_input)): chan0[i] = wav_input[i][0] chan1[i] = wav_input[i][1] time = np.linspace(0, length_in, len(wav_input))plt.plot(time, chan0, label="channel 0")plt.legend()plt.xlabel("Time [s]")plt.ylabel("Amplitude")plt.show()
As you can see in the plot above, the input signal constists of five strums on the guitar. The amplitude of the signal in the plot above is the value of the 16 bit binary data. LTSpice converts the binary data to volts.
The following code reads the csv file saved after the simulation and places the data into a Python dataframe. Notice that the node voltage at \(v_{14}\) has had the 4.5V offset removed.
The data is saved to several NumPy arrays to be processed and displayed using Python. Of interest are the input and output nodes, \(v_1\) and \(v_2\), as well as the nodes \(v_{14}\) and \(v_{16}\).
The following code plots the voltage at nodes \(v_1\) and \(v_2\), which are the input and output nodes of the Klon Centuar. Only the first 54 ms or so is plotted.
The plot above shows the pedal has some gain applied to the signal. The fundamental frequeny appears the same, but the overall waveform is much different, as would be expected since the pedal is modifying the input signal with filtering and ampliude clipping by the diodes.
The plot below shows the voltage plots for node \(v_{14}\), the output terminal of U1b and \(v_{16}\), the voltage on D1.
The plot above shows the amplitude limiting caused by the diodes on the signal.
From here additinal numerical analysis of the node voltages can be performed within the JupyterLab notebook using the signal processing code of NumPy and SciPy. Python’s numerical analysis capability can be combined with circuit simulation results from LTSpice and actual physical measurements of prototype circuits to aid the designer in the analysis and devlopment of electronic circuits.
38.6 Comparison of MNA and LTSpice results
In this section the solutions obtained from MNA and LTSpice will be compared to each other. As shown below the two solutions are very comprable which allows us to have a high degree of confidence in the accuracy of the MNA solution.
The netlist for the MNA solution was obtained from Figure 37.3 and the schematic of Figure 38.1 was used for the LTSpice simulation. For the MNA solution, the diodes, D1 and D2 were removed from the netlist by inserting an asterik at the start of those lines which turn the lines into comments, shown below. Additionally, the MNA netlist treats the Op Amps as ideal Op Amps. In the LTSpice simulation, device modules are used for the diodes and Op Amps. This will add some accuracy to the simulation. The amplitude of the input signal, V1, in the LTSpice simulation was chosen to be \(\pm25 mV\) in the transient simulations, so that the diodes would not be conducting. This keeps both the MNA and the LTSpice solutions in the linear domain.
The MNA solution is presented first, followed by loading reading the exported LTSpice simulation results which were exported as a CSV file. The two solutions are plotted on the same graph so that the results can be compared.
The netlist for the circuit in Figure 37.3, was exported and shown below.
The symbolic modified nodal analysis function is called with the netlist and the MNA results are returned.
Code
report, network_df, i_unk_df, A, X, Z = SymMNA.smna(net_list)
The netlist report provides some statistics about the circuit, which are printed below.
Code
print(report)
Net list report
number of lines in netlist: 56
number of branches: 52
number of nodes: 32
number of unknown currents: 5
number of RLC (passive components): 51
number of resistors: 35
number of capacitors: 16
number of inductors: 0
number of independent voltage sources: 1
number of independent current sources: 0
number of Op Amps: 4
number of E - VCVS: 0
number of G - VCCS: 0
number of F - CCCS: 0
number of H - CCVS: 0
number of K - Coupled inductors: 0
There are 32 nodes and 5 unknown currents in the circuit. This means that the MNA technique will generate a system of 37 equations for SymPy to solve.
The network equations are assembled with the following lines of Python code.
Code
# Put matrices into SymPy X = Matrix(X)Z = Matrix(Z)NE_sym = Eq(A*X,Z)
The system of equations is displayed with the following code using the markdown function.
Code
# display the equationstemp =''for i inrange(shape(NE_sym.lhs)[0]): temp +='${:s} = {:s}$<br>'.format(latex(NE_sym.rhs[i]),latex(NE_sym.lhs[i]))Markdown(temp)
\(0 = I_{V1} + \frac{v_{1}}{R_{1}} - \frac{v_{3}}{R_{1}}\) \(0 = v_{2} \cdot \left(\frac{1}{R_{28}} + \frac{1}{R_{27}} + \frac{1}{R_{26}}\right) - \frac{v_{24}}{R_{27}} - \frac{v_{6}}{R_{26}}\) \(0 = - C_{1} s v_{4} + v_{3} \left(C_{1} s + \frac{1}{R_{1}}\right) - \frac{v_{1}}{R_{1}}\) \(0 = - C_{1} s v_{3} + v_{4} \left(C_{1} s + \frac{1}{R_{2}}\right)\) \(0 = - C_{2} s v_{7} + v_{5} \left(C_{2} s + \frac{1}{R_{4}} + \frac{1}{R_{3}}\right) - \frac{v_{6}}{R_{4}}\) \(0 = v_{6} \cdot \left(\frac{1}{R_{4}} + \frac{1}{R_{26}}\right) - \frac{v_{5}}{R_{4}} - \frac{v_{2}}{R_{26}}\) \(0 = - C_{2} s v_{5} - C_{3} s v_{8} + I_{O1a} + v_{25} \left(- C_{4} s - \frac{1}{R_{5}}\right) + v_{7} \left(C_{2} s + C_{3} s + C_{4} s + \frac{1}{R_{5}}\right)\) \(0 = - C_{3} s v_{7} + v_{8} \left(C_{3} s + C_{5} s + \frac{1}{R_{7}} + \frac{1}{R_{6}}\right) + v_{9} \left(- C_{5} s - \frac{1}{R_{6}}\right) - \frac{v_{10}}{R_{7}}\) \(0 = v_{8} \left(- C_{5} s - \frac{1}{R_{6}}\right) + v_{9} \left(C_{5} s + \frac{1}{Rp1a1} + \frac{1}{R_{6}}\right)\) \(0 = v_{10} \left(C_{16} s + \frac{1}{R_{7}} + \frac{1}{R_{24}}\right) - \frac{v_{8}}{R_{7}} - \frac{v_{18}}{R_{24}}\) \(0 = v_{11} \cdot \left(\frac{1}{Rp1b1} + \frac{1}{R_{10}}\right) - \frac{v_{12}}{R_{10}}\) \(0 = v_{12} \left(C_{7} s + \frac{1}{R_{11}} + \frac{1}{R_{10}}\right) + v_{13} \left(- C_{7} s - \frac{1}{R_{11}}\right) - \frac{v_{11}}{R_{10}}\) \(0 = v_{12} \left(- C_{7} s - \frac{1}{R_{11}}\right) + v_{13} \left(C_{7} s + C_{8} s + \frac{1}{R_{12}} + \frac{1}{R_{11}}\right) + v_{14} \left(- C_{8} s - \frac{1}{R_{12}}\right)\) \(0 = - C_{9} s v_{15} + I_{O1b} + v_{13} \left(- C_{8} s - \frac{1}{R_{12}}\right) + v_{14} \left(C_{8} s + C_{9} s + \frac{1}{R_{12}}\right)\) \(0 = - C_{9} s v_{14} + v_{15} \left(C_{9} s + \frac{1}{R_{13}}\right) - \frac{v_{16}}{R_{13}}\) \(0 = - C_{10} s v_{17} + v_{16} \left(C_{10} s + \frac{1}{R_{13}}\right) - \frac{v_{15}}{R_{13}}\) \(0 = - C_{10} s v_{16} - C_{11} s v_{28} + v_{17} \left(C_{10} s + C_{11} s + \frac{1}{R_{16}}\right) - \frac{v_{18}}{R_{16}}\) \(0 = - C_{12} s v_{29} + v_{18} \left(C_{12} s + C_{13} s + \frac{1}{R_{24}} + \frac{1}{R_{20}} + \frac{1}{R_{17}} + \frac{1}{R_{16}}\right) + v_{19} \left(- C_{13} s - \frac{1}{R_{20}}\right) - \frac{v_{10}}{R_{24}} - \frac{v_{27}}{R_{17}} - \frac{v_{17}}{R_{16}}\) \(0 = I_{O2a} + v_{18} \left(- C_{13} s - \frac{1}{R_{20}}\right) + v_{19} \left(C_{13} s + \frac{1}{R_{22}} + \frac{1}{R_{21}} + \frac{1}{R_{20}}\right) - \frac{v_{20}}{R_{22}} - \frac{v_{32}}{R_{21}}\) \(0 = - C_{14} s v_{30} + v_{20} \left(C_{14} s + \frac{1}{R_{241}} + \frac{1}{R_{22}}\right) - \frac{v_{21}}{R_{241}} - \frac{v_{19}}{R_{22}}\) \(0 = - C_{15} s v_{22} + I_{O2b} + v_{21} \left(C_{15} s + \frac{1}{R_{241}} + \frac{1}{R_{23}}\right) - \frac{v_{20}}{R_{241}} - \frac{v_{31}}{R_{23}}\) \(0 = - C_{15} s v_{21} + v_{22} \left(C_{15} s + \frac{1}{R_{25}}\right) - \frac{v_{23}}{R_{25}}\) \(0 = v_{23} \cdot \left(\frac{1}{Rp3a} + \frac{1}{R_{25}}\right) - \frac{v_{24}}{Rp3a} - \frac{v_{22}}{R_{25}}\) \(0 = v_{24} \cdot \left(\frac{1}{Rp3b} + \frac{1}{Rp3a} + \frac{1}{R_{27}}\right) - \frac{v_{23}}{Rp3a} - \frac{v_{2}}{R_{27}}\) \(0 = - C_{6} s v_{26} + v_{25} \left(C_{4} s + C_{6} s + \frac{1}{Rp1b2} + \frac{1}{R_{8}} + \frac{1}{R_{5}}\right) + v_{7} \left(- C_{4} s - \frac{1}{R_{5}}\right) - \frac{v_{27}}{Rp1b2}\) \(0 = - C_{6} s v_{25} + v_{26} \left(C_{6} s + \frac{1}{R_{9}}\right)\) \(0 = v_{27} \cdot \left(\frac{1}{Rp1b2} + \frac{1}{Rp1a2} + \frac{1}{R_{18}} + \frac{1}{R_{17}} + \frac{1}{R_{15}}\right) - \frac{v_{25}}{Rp1b2} - \frac{v_{29}}{R_{18}} - \frac{v_{18}}{R_{17}} - \frac{v_{28}}{R_{15}}\) \(0 = - C_{11} s v_{17} + v_{28} \left(C_{11} s + \frac{1}{R_{15}}\right) - \frac{v_{27}}{R_{15}}\) \(0 = - C_{12} s v_{18} + v_{29} \left(C_{12} s + \frac{1}{R_{18}}\right) - \frac{v_{27}}{R_{18}}\) \(0 = - C_{14} s v_{20} + v_{30} \left(C_{14} s + \frac{1}{Rp2b} + \frac{1}{Rp2a}\right) - \frac{v_{31}}{Rp2b} - \frac{v_{32}}{Rp2a}\) \(0 = v_{31} \cdot \left(\frac{1}{Rp2b} + \frac{1}{R_{23}}\right) - \frac{v_{30}}{Rp2b} - \frac{v_{21}}{R_{23}}\) \(0 = v_{32} \cdot \left(\frac{1}{Rp2a} + \frac{1}{R_{21}}\right) - \frac{v_{30}}{Rp2a} - \frac{v_{19}}{R_{21}}\) \(V_{1} = v_{1}\) \(0 = - v_{4} + v_{7}\) \(0 = v_{13} - v_{9}\) \(0 = v_{18}\) \(0 = v_{20}\)
There are too many equations and symbols for SymPy to obtain a symbolic solution and symbolic result would not be useful.
The following code is used to turn the free symbols into SymPy variables and the numeric values for the components are put into a Python dictionary.
As shown below, the value of V1 is set to one. The value of \(V_1\) gets cancelled in the voltage transfer function, so this value does not effect the solution. The bypass switch is put into the effects position by setting the values of \(R_{26}\) and \(R_{27}\). The controls on the Klon Centuar are set to mid position.
The following code replaces the symbols in the network equations with numerical values and displays the equations.
Code
NE = NE_sym.subs(element_values)temp =''for i inrange(shape(NE.lhs)[0]): temp +='${:s} = {:s}$<br>'.format(latex(NE.rhs[i]),latex(NE.lhs[i]))Markdown(temp)
Read the frequency response data from LTSpice so the LTSpice and Python results plots can be overlaid for comparison.
Code
fn ='Klon-Centaur_v2_freq_resp.csv'# data from LTSpiceLTSpice_data = np.genfromtxt(fn, delimiter=',',skip_header=1)# initaliaze some empty arraysfrequency = np.zeros(len(LTSpice_data))H_v2 = np.zeros(len(LTSpice_data)).astype(complex)# convert the csv data to complex numbers and store in the arrayfor i inrange(len(LTSpice_data)): frequency[i] = LTSpice_data[i][0] H_v2[i] = LTSpice_data[i][1] + LTSpice_data[i][2]*1j
Plot the results using np.unwrap(2 * phase) / 2) to keep the phase plots the same.
Code
fig, ax1 = plt.subplots()ax1.set_ylabel('magnitude, dB')ax1.set_xlabel('frequency, Hz')plt.semilogx(frequency[0:-1], 20*np.log10(np.abs(H_v2)[0:-1]),'-k',label='LTSpice sch v2 mag') # LTSpice magnitude plotplt.semilogx(w/(2*np.pi), mag,'-r',label='MNA mag, dB') # MNA magnitude plotax1.tick_params(axis='y')#ax1.set_ylim((-30,20))plt.grid()plt.legend(loc='upper right')# instantiate a second y-axes that shares the same x-axisax2 = ax1.twinx()color ='tab:blue'plt.semilogx(frequency[0:-1], np.unwrap(2*np.angle(H_v2)[0:-1]/2) *180/np.pi,':',color='k',label='LTSpice phase') # LTSpice phase plotplt.semilogx(w/(2*np.pi), phase,':',color='r',label='MNA phase') # MNA phase plotplt.legend(loc='lower right')ax2.set_ylabel('phase, deg',color=color)ax2.tick_params(axis='y', labelcolor=color)#ax2.set_ylim((-5,25))# highlight the guitar audio band, 80 to 8kHzplt.axvspan(80, 8e3, color='y', alpha=0.3)plt.title('Magnitude and phase response')plt.show()
Figure 38.10: LTSpice and MNA data magnitude and phase response plotted for comparison
As shown above, there is a slight difference between the MNA results and the LTSpice simulation, but generally there is good agreement. The LTSpice simulation is using device models for the diodes and Op Amps which accounts for the slight differences.
The poles and zeros of the transfer function can easly be obtained with the following code:
The poles and zeros of the voltage transfer function are plotted on the complex frequency plane.
Code
plt.plot(np.real(sys_zeros), np.imag(sys_zeros), 'ob', markerfacecolor='none')plt.plot(np.real(sys_poles), np.imag(sys_poles), 'xr')plt.legend(['Zeros', 'Poles'], loc=0)plt.title('Complex plane Pole / Zero Plot')plt.xlabel('real part, \u03C3')plt.ylabel('imaginary part, j\u03C9')plt.grid()plt.show()
Poles and zeros of the transfer function plotted on the complex plane and the units are in radian frequency. These values are printed in the following table.
Code
table_header = ['Zeros, rad/s', 'Poles, rad/s']num_table_rows =max(len(sys_zeros),len(sys_poles))table_row = []for i inrange(num_table_rows):if i <len(sys_zeros): z ='{:,.2f}'.format(sys_zeros[i])else: z =''if i <len(sys_poles): p ='{:,.2f}'.format(sys_poles[i])else: p ='' table_row.append([z,p])print(tabulate(table_row, headers=table_header,colalign = ('left','left'),tablefmt="simple"))
The SciPy function lsim was used to calculate the time domain response to a 10 Hz square wave. The input amplitude is scaled and level shifted to produce a 0 to 50 mV signal which keeps the levels at node 16 below the diode conduction level.
The plot above shows the input square wave in red and the out signal in blue. The signal path is AC coupled as indicated by the lack of DC response in the plot. The decay time is a bit over 25 ms.
At the leading edge of the square wave input, there is about a 250 mV spike, which susgests a large AC gain in the circuit because the abrupt change in the input signal at the leading edge of steps of the square wave produce substantial harmonic componets within the passband of the pedal’s frequency response.
The square wave simultion data from LTSpice was loaded in the JupyterLab notebook so that the data could be overlaid along with the NMA solution.
Code
fn ='Klon-Centaur_v2_square_wave.csv'# data from LTSpiceLTSpice_data = np.genfromtxt(fn, delimiter=',',skip_header=1)# initaliase some empty arraystime = np.zeros(len(LTSpice_data))voltage1 = np.zeros(len(LTSpice_data))voltage2 = np.zeros(len(LTSpice_data))# convert the csv data to complez numbers and store in the arrayfor i inrange(len(LTSpice_data)): time[i] = LTSpice_data[i][0] voltage1[i] = LTSpice_data[i][1] voltage2[i] = LTSpice_data[i][2]
The code below generates the plots for the LTSpice and Python MNA data.
As shown above, there is close agrement between the two solutions in both the frequency and time domain for inout signal levels and gain settings that do not produce node voltages at node 16 with levels that would drive the diodes into conduction.
38.7 Summary for part 2
The results of LTSpice simulations were used to examine the operation of the Klon Centuar circuit.